package com.ar4j;

import java.lang.reflect.Method;
import java.util.Map;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import com.ar4j.ActiveRecordMetadata.ActiveRecordMethodType;
import com.ar4j.util.ClassUtils;

/**
 * Main delegating invocation handler for all active record proxies. Simply uses the map in the given
 * active record meta data to invoke a specific IMethodHandler for the current active record class.
 */
public class DelegatingMethodInterceptor implements MethodInterceptor {
  private ActiveRecordMetadata metadata;
  private Map<Method, TransactionAttribute> methodTransactionAttributeMap;
  private Map<Method, ActiveRecordMethodType> methodTypeMap;
  private Map<Method, Method> defaultImplementationMethodMap;
  private Map<Method, DynamicMethodMetadata> dynamicMethodMetadataMap;
  private Map<String, Method> propertyReadMethods;
  private Map<String, Method> propertyWriteMethods;
  private ActiveRecordContext context;
  private ActiveRecord<?> defaultImplementation;
  
  public DelegatingMethodInterceptor(ActiveRecordMetadata metadata, ActiveRecordContext context) {
    this.metadata = metadata;
    this.methodTransactionAttributeMap = metadata.getMethodTransactionAttributeMap();
    this.methodTypeMap = metadata.getMethodTypeMap();
    this.defaultImplementationMethodMap = metadata.getDefaultImplementationMethodMap();
    this.dynamicMethodMetadataMap = metadata.getDynamicMethodMetadataMap();
    this.propertyReadMethods = metadata.getPropertyReadMethods();
    this.propertyWriteMethods = metadata.getPropertyWriteMethods();
    this.context = context;
  }
  
  public ActiveRecord<?> getDefaultImplementation() {
    return defaultImplementation;
  }

  public void setDefaultImplementation(ActiveRecord<?> defaultImplementation) {
    this.defaultImplementation = defaultImplementation;
  }

  @Override
  public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable {
    final ActiveRecord<?> dataHolder = (ActiveRecord<?>) obj;
    TransactionAttribute transactionAttribute = methodTransactionAttributeMap.get(method);
    Object out = null;
    if(transactionAttribute != null && transactionAttribute.getPropagationBehavior() != TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
      if(context.getPlatformTransactionManager() == null) {
        throw new IllegalStateException("Cannot execute transactional active record method without a platform transaction manager in the context, method: " + method);
      }
      
      TransactionTemplate template = new TransactionTemplate(context.getPlatformTransactionManager(), transactionAttribute);
      out = template.execute(new TransactionCallback<Object>() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
          try { return handleCall(method, obj, args, dataHolder, proxy); }
          catch(Throwable e) { throw new RuntimeException("An error occured executing handler within a transaction", e); }
        }
      });
    } else {
      out = handleCall(method, obj, args, dataHolder, proxy);
    }
    
    return out;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }

  /**
   * Handle a call to an active record method, multiplexing by the method type to figure out if we should
   * let the invocation pass through to the instance, execute it on the default implementation, or execute
   * via dynamic method handler.
   */
  private Object handleCall(Method method, Object obj, Object[] args, ActiveRecord<?> dataHolder, MethodProxy proxy) 
    throws Throwable {
    
    Object out = null;
    ActiveRecordMethodType type = methodTypeMap.get(method);
    if(type == ActiveRecordMethodType.GET_PROPERTY) {
      out = handleGetProperty(obj, args);
      
    } else if(type == ActiveRecordMethodType.SET_PROPERTY) {
      out = handleSetProperty(obj, args);
      
    } else if(type == ActiveRecordMethodType.PASSTHROUGH) {
      out = proxy.invokeSuper(obj, args);
      
    } else if(type == ActiveRecordMethodType.HANDLED_BY_DEFAULT_IMPLEMENTATION) {
      out = defaultImplementationMethodMap.get(method).invoke(defaultImplementation, args);
      
    } else if(type == ActiveRecordMethodType.DYNAMIC) {
      out = DynamicMethodHandler.handleDynamicMethod(metadata, context, dynamicMethodMetadataMap.get(method), dataHolder, args);
      
    } else {
      throw new RuntimeException("Unknown type of method: " + method);
    }
    
    return out;
  }
  
  /**
   * Handler that is able to retrieve an active record's property value via it's read method.
   */
  private Object handleGetProperty(Object target, Object[] args) 
    throws Throwable {
    
    // first arg must be the property name
    //String name = (String) args[0];
    
    // find the read method, error out if there is none
    Method readMethod = propertyReadMethods.get((String) args[0]);
    if(readMethod == null) {
      throw new IllegalArgumentException("Cannot get property value for " + (String) args[0] + " in " + metadata.getBaseClass() + " because the read method is missing");
    }
    
    // invoke and get the value
    Object value = readMethod.invoke(target);
    
    // coerce to a type if we were given one
    if(args.length > 1) {
      Class<?> requiredType = (Class<?>) args[1];
      value = ClassUtils.coerceValueToType(value, requiredType);
    }
    
    return value;
  }
  
  /**
   * Handler that is able to set an active record's property value via it's write method.
   */
  private Object handleSetProperty(Object target, Object[] args) 
    throws Throwable {
    
    // first arg must be the property name
    //String name = (String) args[0];
    
    // find the write method, error out if there is none
    Method writeMethod = propertyWriteMethods.get((String) args[0]);
    if(writeMethod == null) {
      throw new IllegalArgumentException("Cannot set property value for " + (String) args[0] + " in " + metadata.getBaseClass() + " because the write method is missing");
    }
    
    // invoke and get the value
    writeMethod.invoke(target, args[1]);
    
    // always return the target itself
    return target;
  }
}
